﻿#region Using Statements
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#endregion // Using Statements

namespace ParticleSystem
{
    /// <summary>
    /// Object describing a single particle in the particle system.
    /// </summary>
    public class Particle
    {
        #region Fields
        /// <summary>
        /// This particle's id, uniquely defining it in the system.
        /// </summary>
        private int mID;
        public int id
        {
            get { return mID; }
        }

        /// <summary>
        /// The lifespan of this particle.
        /// </summary>
        private TimeSpan mLifespan;
        public TimeSpan lifespan
        {
            get { return mLifespan; }
        }

        /// <summary>
        /// The age of this particle.
        /// </summary>
        private float mAge;
        public float age
        {
            get { return mAge; }
        }

        /// <summary>
        /// This particle's current position.
        /// </summary>
        private Vector3 mPosition;
        public Vector3 position
        {
            get { return mPosition; }
        }

        /// <summary>
        /// The previous position occupied by this particle.
        /// </summary>
        private Vector3 mPrevPosition;

        /// <summary>
        /// The particle's current velocity.
        /// </summary>
        private Vector3 mVelocity;

        /// <summary>
        /// This particle's current color.
        /// </summary>
        private Color mColor;
        public Color color
        {
            get { return mColor; }
        }

        /// <summary>
        /// This particle's starting radius.
        /// </summary>
        private float mStartRadius;

        /// <summary>
        /// This particle's ending radius.
        /// </summary>
        private float mEndRadius;

        /// <summary>
        /// This particle's current radius.
        /// </summary>
        private float mRadius;
        public float radius
        {
            get { return mRadius; }
        }

        /// <summary>
        /// The behavior this particle should take when it collides with something.
        /// </summary>
        private CollisionBehavior mCollisionBehavior;

        /// <summary>
        /// Any forces specific to this system acting on this particle.
        /// </summary>
        private List<Force> mForces;

        /// <summary>
        /// Flag for whether or not this particle is stuck on something.
        /// </summary>
        private bool mIsStuck;

        #endregion // Fields

        #region Constructors
        /// <summary>
        /// Particle constructor.
        /// </summary>
        /// <param name="graphicsDevice">The graphics device on which this particle will be drawn.</param>
        /// <param name="id">The unique ID for this particle.</param>
        /// <param name="lifespan">The lifespan (in frames) for this particle.</param>
        /// <param name="position">The particle's starting position.</param>
        /// <param name="startVelocity">The particle's starting velocity.</param>
        /// <param name="color">The particle's starting color.</param>
        /// <param name="radius">The particle's radius.</param>
        /// <param name="collisionBehavior">The particle's behavior when it collides with something.</param>
        public Particle(int id, TimeSpan lifespan, Vector3 position, Vector3 startVelocity, Color color, float startRadius, float endRadius, CollisionBehavior collisionBehavior, List<Force> forces)
        {
            mID = id;
            mLifespan = lifespan;
            mAge = 0;
            mPrevPosition = mPosition = position;
            mVelocity = startVelocity;
            mColor = color;
            mStartRadius = mRadius = startRadius;
            mEndRadius = endRadius;
            mCollisionBehavior = collisionBehavior;
            mForces = forces;
            mIsStuck = false;
        }
        #endregion // Constructors

        #region Update
        /// <summary>
        /// Update the particle.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        /// <param name="forces">The forces acting upon this particle.</param>
        /// <param name="collisionPlanes">The planes in the world that particles may collide with.</param>
        public void Update(GameTime gameTime, List<Force> forces, List<DrawablePlane> collisionPlanes)
        {
            float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;

            // update particle's age
            mAge += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

            // use the particle's normalized age (ranging between 0 and 1
            float normalizedAge = mAge / (float)mLifespan.TotalMilliseconds;
            normalizedAge = MathHelper.Clamp(normalizedAge, 0, 1);

            // update particle size
            UpdateRadius(normalizedAge);

            // update particle color
            UpdateColor(normalizedAge);

            // update particle position
            UpdatePosition(elapsedSeconds, normalizedAge, forces, collisionPlanes);
        }

        /// <summary>
        /// Update this particle's size.
        /// </summary>
        /// <param name="normalizedAge">The particle's normalized age (ranges between 0 and 1).</param>
        private void UpdateRadius(float normalizedAge)
        {
            mRadius = MathHelper.Lerp(mStartRadius, mEndRadius, normalizedAge);
        }

        /// <summary>
        /// Update this particle's color.
        /// </summary>
        /// <param name="normalizedAge">The particle's normalized age (ranges between 0 and 1).</param>
        private void UpdateColor(float normalizedAge)
        {
            // Fade the particle's alpha based on its normalized age (ranges between 0 and 1)
            // Curve is hardcoded to make the particle fade in fairly quickly, then fade out more slowly.
            // The 6.7 scaling factor normalizes the curve so the alpha will reach all the way up to fully solid.

            // The code is commented out right now because it's causing artifacts when combined with premultiplying in the alpha value.
            //float newAlpha = (float)(255 * normalizedAge * (1 - normalizedAge) * (1 - normalizedAge) * 6.7);
            //mColor.A = (byte)MathHelper.Clamp(newAlpha, 0, 255);
        }

        /// <summary>
        /// Update this particle's position.
        /// </summary>
        /// <param name="elapsedSeconds">Elapsed seconds since the last update.</param>
        /// <param name="normalizedAge">The particle's normalized age (ranges between 0 and 1).</param>
        /// <param name="forces">External forces acting on this particle.</param>
        private void UpdatePosition(float elapsedSeconds, float normalizedAge, List<Force> forces, List<DrawablePlane> collisionPlanes)
        {
            // if the particle is stuck, we don't need to update its position
            if (mIsStuck)
                return;

            // account for particle-specific forces in the particle's current velocity
            foreach (Force particleForce in mForces)
            {
                mVelocity += particleForce.getActingForce(mVelocity) * elapsedSeconds;
            }

            // account for external forces in the particles's current velocity
            foreach (Force force in forces)
            {
                mVelocity += force.getActingForce(mVelocity) * elapsedSeconds;
            }

            // record the last position occupied by this particle before updating it
            mPrevPosition = mPosition;

            // update the particle's position based on its velocity
            mPosition += mVelocity * elapsedSeconds;

            // finally, we need to handle any plane collisions that occurred
            HandleCollisions(collisionPlanes);
        }
        #endregion // Update

        #region Collision Detection and Handling
        /// <summary>
        /// Detect and handle collisions with any planes in the world.
        /// </summary>
        /// <param name="collisionPlanes">List of collision planes that exist in the particle's world.</param>
        private void HandleCollisions(List<DrawablePlane> collisionPlanes)
        {
            foreach (DrawablePlane drawablePlane in collisionPlanes)
            {
                float oldDistanceToPlane = Vector3.Dot(Vector3.Normalize(drawablePlane.plane.Normal), mPrevPosition) - drawablePlane.plane.D;
                float currentDistanceToPlane = Vector3.Dot(Vector3.Normalize(drawablePlane.plane.Normal), mPosition) - drawablePlane.plane.D;

                // if the signs of the distances are not the same, 
                // then the particle passed through the plane on this update
                if (Math.Sign(oldDistanceToPlane) != Math.Sign(currentDistanceToPlane))
                {
                    // find the point at which the particle and the plane intersected
                    Ray particleRay = new Ray(mPrevPosition, Vector3.Normalize(mVelocity));
                    Plane correctedPlane = (oldDistanceToPlane < 0) ? new Plane(-drawablePlane.plane.Normal, -drawablePlane.plane.D) : drawablePlane.plane;
                    float? intersectionDistance = particleRay.Intersects(correctedPlane);

                    // found the ray-plane intersection distance
                    if (intersectionDistance.HasValue)
                    {
                        Vector3 intersectionPoint = particleRay.Position + intersectionDistance.Value * particleRay.Direction;

                        // respond to collision according to specified collision behavior
                        switch (mCollisionBehavior)
                        {
                            case CollisionBehavior.BOUNCE:
                                // keep the particle on the inside of the bounding plane by adding a fracting of the inward pointing plane normal
                                mPosition = intersectionPoint + (correctedPlane.Normal * 0.1f);
                                // reflect the particle's velocity across the collision plane's normal to simulate bouncing
                                mVelocity = Vector3.Reflect(mVelocity, correctedPlane.Normal);
                                mVelocity *= 0.7f; // account for absorption caused by the collision
                                break;
                            case CollisionBehavior.DIE:
                                mPosition = intersectionPoint;
                                // effectively kill this particle by setting its age equal to its lifespan
                                mAge = (float)mLifespan.TotalMilliseconds;
                                break;
                            case CollisionBehavior.STICK:
                                mPosition = intersectionPoint;
                                mIsStuck = true;
                                break;
                        }
                    }
                }
            } // end foreach (Plane plane in collisionPlanes)
        }
        #endregion // Collision Detection and Handling
    } // end Particle class definition

    /// <summary>
    /// The different types of behaviors a particle may have when it collides with something.
    /// </summary>
    public enum CollisionBehavior
    {
        BOUNCE,
        STICK,
        DIE
    };
}
